24 药品销售数据分析
24.1 引言药品销售数据分析的价值
医院药品销售分析:
- 库存管理: 优化药品采购
- 处方分析: 医生开药习惯
- 医保分析: 社保使用情况
- 销售预测: 季节性需求
24.2 本章学习目标
本章以医院药品销售流水为对象,练习围绕业务提问组织分析。通过本章学习,你将掌握:
- Excel销售数据的读取与体检:
shape、head、info、缺失值与重复值检查 - 从”购药时间”字符串切分日期与星期、再从日期提取月份的特征构造方法
groupby+sum+nlargest/nsmallest的排名式聚合- 用
sns.lineplot(estimator='sum')与柱状图呈现月度、星期销售结构与TOP榜 - 用”应收金额-实收金额”的差额构造社保减免标志并统计占比
24.3 数据预处理
任务要求:读取平台内置的”朝阳医院2018年销售数据.xlsx”,完成类型转换与缺失值、重复值检查后,依次回答四个业务问题——(1)按实收金额总和找出销售最好与最差的药品,并绘制销售金额TOP10柱状图;(2)从购药时间提取月份与星期,分别绘制月度、星期销售额折线图;(3)逐月找出当月实收金额最高的药品;(4)用应收金额与实收金额的差额构造”是否社保减免药品”标志,并统计其占比。
以下平台原始代码在任务3的循环体内存在缩进错误,属平台题面原样,请按平台原始题面原样输入,以平台判定为准。
# 注:朝阳医院2018年销售数据.xlsx数据文件本地没有,但平台已经内置
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
# 注:该代码块存在缩进错误,请按平台原始题面原样输入,以平台判定为准
import pandas as pd # 导入Pandas数据分析库
import numpy as np # 导入NumPy数值计算库
import seaborn as sns # 导入Seaborn可视化库
import matplotlib.pyplot as plt # 导入Matplotlib绘图库
plt.rcParams["font.sans-serif"] = ["SimHei"] # 设置Matplotlib全局参数
#数据读取
df = pd.read_excel("朝阳医院2018年销售数据.xlsx")
#数据预览与预处理
## 查看数据维度
print(df.shape)
print(df.head()) # 输出前几行数据
## 查看数据信息
print(df.info())
df['社保卡号']=df['社保卡号'].astype(str) # 转换数据类型
df['商品编码']=df['商品编码'].astype(str) # 转换数据类型
df['购药日期']=df['购药时间'].str.split(pat=' ',expand=True)[0] # 对字符串列进行处理
df['购药星期']=df['购药时间'].str.split(pat=' ',expand=True)[1] # 对字符串列进行处理
## 查看各列的缺失值
df.isnull().sum()
## 缺失值处理
## 直接删除
df.dropna(axis=0,inplace=True)
## 查看重复值数量
df.duplicated().sum()
#探索性数据分析
##任务1:销售情况最好的药品是?最差的药品是?
##提示:通过对药品进行分组聚合统计,在此处我们评定销售情况好的标准是实收金额总和。之后取出前n个药品进行可视化展示
df_sum=df.groupby('商品名称')['实收金额'].sum().reset_index() # 按指定列分组聚合
print(df_sum.nlargest(2,'实收金额')) #用于获取实收金额列中最大的两个值所对应的行。
print(df_sum.nsmallest(2,'实收金额')) #用于获取实收金额列中最小的两个值所对应的行。
df_sum_n10=df_sum.nlargest(10,'实收金额') # 筛选出排名前10的记录
plt.figure(figsize=(15,10)) # 创建图形画布
plt.grid() # 显示网格线
plt.bar(df_sum_n10['商品名称'],df_sum_n10['实收金额']) # 绘制柱状图
plt.savefig("1.png") # 保存图形至文件
plt.close() # 关闭文件或连接
#任务2:整体销售额的时间变化趋势
##数据集中存在时间数据,但是不够简洁,所以需要先对时间数据进行处理:
df["购药时间_月"] = df["购药时间"].map(lambda x:x.split(" ")[0].split("-")[1]) ##购药日期所属月份
df["购药时间_星期"] = df["购药时间"].map(lambda x:x.split(" ")[1]) ##购药当天是星期几?
#对时间数据处理完成之后,就可以进行时间维度上的销售情况分析,与任务1一致,评定指标也选择为实收金额之和。
# 在下方开始你的分析
df['购药日期_月']=df['购药日期'].str.split(pat='-',expand=True)[1]
plt.figure(figsize=(15,10)) # 创建图形画布
plt.grid() # 显示网格线
sns.lineplot(x='购药日期_月',y='实收金额',data=df,estimator='sum') # 绘制折线图
plt.savefig("2.png") # 保存图形至文件
plt.close() # 关闭文件或连接
plt.figure(figsize=(15,10)) # 创建图形画布
plt.grid() # 显示网格线
sns.lineplot(x='购药星期',y='实收金额',data=df,estimator='sum') # 绘制折线图
plt.savefig("3.png") # 保存图形至文件
plt.close() # 关闭文件或连接
#任务3:各月份卖的最好的药品是那些?
values1=df['购药日期_月'].unique()
values1=pd.Series(values1) # 创建Series序列values1
for value in values1: # 遍历values1中的每个value
df_value=df[df['购药日期_月']==value] # 提取购药日期_月列作为df_value变量
df_print=df_value.groupby(['购药日期_月','商品名称'])['实收金额'].sum().nlargest(1).reset_index() # 按指定列分组聚合
print(df_print) # 输出数据框数据
#任务4:人们是否会更愿意购买可使用社保减免的药品?
df['差额']=df['应收金额']-df['实收金额']
df['是否社保减免药品']=df['差额'].apply(lambda x : '是' if x!=0 else '否') # 定义匿名函数df['是否社保减免药品']
df['是否社保减免药品'].value_counts(normalize=True) # 统计是否社保减免药品列各取值的频率分布
#####总结
# 卖的最好的是 开博通
# 卖的最差的是 TG厄贝沙坦片
# 药品的销售数量与时间有关
# 药品的销售数量与是否使用社保有关预期输出:数据文件平台内置,本地无数据,具体数值以平台运行结果为准。判读要点:
- 文本输出:数据形状、
info摘要、各列缺失值计数;销售最好与最差的各2种药品;逐月的当月最佳药品清单;“是否社保减免药品”的占比频率表。 - 图形输出(1.png):销售金额TOP10药品柱状图——看头部药品之间的量级差距与断层。图形输出(2.png、3.png):月度、星期销售额折线图——看峰值月份、低谷月份,以及工作日与周末的差异。
- 逐月最佳药品清单应与月度折线图互证:清单里的药品是否正是把对应月份销售额抬高的主力。
- 平台题面在任务3的循环体内存在缩进错误,按平台原始题面原样输入,以平台判定为准。
下方提供一个本地演练版:由于”朝阳医院2018年销售数据.xlsx”仅平台内置,本地以一份 12 行的同结构内联合成迷你表代替(列名与平台数据一致,购药时间同样为”日期 空格 星期”格式,部分行应收金额大于实收金额以保留社保减免的差额形态),并在其上复现平台任务1”按实收金额对药品排名”的同型输出:
# 本地演练说明:平台内置的朝阳医院2018年销售数据.xlsx本地没有,以下用同结构的内联合成迷你数据代替
import pandas as pd # 导入Pandas数据分析库
df_mini = pd.DataFrame({ # 构造12行迷你表,列结构同平台数据
'社保卡号': ['1001', '1002', '1003', '1004', '1005', '1006', '1007', '1008', '1009', '1010', '1011', '1012'], # 社保卡号(文本型)
'商品编码': ['P001', 'P002', 'P003', 'P001', 'P002', 'P004', 'P001', 'P003', 'P002', 'P004', 'P001', 'P003'], # 商品编码(文本型)
'商品名称': ['开博通', '氨苄西林胶囊', 'TG厄贝沙坦片', '开博通', '氨苄西林胶囊', 'VC银翘片',
'开博通', 'TG厄贝沙坦片', '氨苄西林胶囊', 'VC银翘片', '开博通', 'TG厄贝沙坦片'], # 商品名称
'销售数量': [5, 2, 1, 3, 4, 2, 6, 1, 3, 2, 4, 1], # 销售数量
'应收金额': [316.0, 74.4, 19.8, 189.6, 148.8, 45.6, 379.2, 19.8, 111.6, 45.6, 252.8, 19.8], # 应收金额(原价)
'实收金额': [281.0, 66.96, 19.8, 189.6, 133.92, 45.6, 379.2, 19.8, 100.44, 45.6, 252.8, 19.8], # 实收金额(部分行小于应收,即社保减免)
'购药时间': ['2018-01-05 星期五', '2018-01-08 星期一', '2018-01-12 星期五', '2018-02-02 星期五', '2018-02-05 星期一', '2018-02-09 星期五',
'2018-03-02 星期五', '2018-03-05 星期一', '2018-03-09 星期五', '2018-03-12 星期一', '2018-03-16 星期五', '2018-03-19 星期一'] # 日期+星期,同平台格式
})
df_sum = df_mini.groupby('商品名称')['实收金额'].sum().reset_index() # 与平台任务同口径:按商品名称聚合实收金额总和
print('按实收金额的药品排名(降序):')
print(df_sum.sort_values('实收金额', ascending=False).reset_index(drop=True)) # 全量排名表
print('销售最好的2种药品:')
print(df_sum.nlargest(2, '实收金额').reset_index(drop=True)) # 前两名,同平台任务1
print('销售最差的2种药品:')
print(df_sum.nsmallest(2, '实收金额').reset_index(drop=True)) # 后两名,同平台任务1预期输出(本机实际运行结果):
按实收金额的药品排名(降序):
商品名称 实收金额
0 开博通 1102.60
1 氨苄西林胶囊 301.32
2 VC银翘片 91.20
3 TG厄贝沙坦片 59.40
销售最好的2种药品:
商品名称 实收金额
0 开博通 1102.60
1 氨苄西林胶囊 301.32
销售最差的2种药品:
商品名称 实收金额
0 TG厄贝沙坦片 59.4
1 VC银翘片 91.2
判读要点与平台任务1同型:开博通按实收金额总和居首、TG厄贝沙坦片垫底;nlargest(2)/nsmallest(2) 的结果与降序排名表的头尾两行互相印证。
24.4 任务1销售情况最好的药品
平台判定代码按各药品的实收金额总和聚合排定销售名次(题面提示原文为“销售数量总和”,与判定代码口径不一致时以平台判定为准)。口径既然如此关键,本节不再逐行重述任务1的聚合与绘图代码,改做一个变式实验:把聚合列这一个参数由 实收金额 换成 销售数量,其余代码一行不动,再看”销售最好的药品”还是不是同一个答案。实验在本地合成数据上运行(15行,列结构同平台数据;其中VC银翘片被设定为单价约10元的低价常用药、开博通为单价约60元的高价药,以放大两种口径的差异),请重点观察对比表中”金额口径名次”与”数量口径名次”两列的错位。
# 本地演练说明:朝阳医院2018年销售数据.xlsx仅平台内置,以下15行合成数据与平台数据同结构
import pandas as pd # 导入Pandas数据分析库
df_rank = pd.DataFrame({ # 15行合成数据,列结构同平台数据;VC银翘片为低价常用药、开博通为高价药,以放大两种口径的差异
'商品名称': ['开博通', '开博通', '开博通', '开博通', '氨苄西林胶囊', '氨苄西林胶囊', '氨苄西林胶囊', 'TG厄贝沙坦片', 'TG厄贝沙坦片', 'TG厄贝沙坦片', 'VC银翘片', 'VC银翘片', 'VC银翘片', 'VC银翘片', 'VC银翘片'], # 商品名称
'销售数量': [5, 3, 6, 4, 2, 4, 3, 1, 1, 1, 2, 2, 5, 5, 5], # 每笔销售的盒数
'应收金额': [316.0, 189.6, 379.2, 252.8, 74.4, 148.8, 111.6, 19.8, 19.8, 19.8, 20.52, 20.52, 51.3, 51.3, 51.3], # 应收金额(原价)
'实收金额': [281.0, 189.6, 379.2, 252.8, 66.96, 133.92, 100.44, 19.8, 19.8, 19.8, 20.52, 20.52, 51.3, 51.3, 51.3], # 实收金额(部分行小于应收,即社保减免)
'购药时间': ['2018-01-05 星期五', '2018-02-02 星期五', '2018-03-02 星期五', '2018-03-16 星期五', '2018-01-08 星期一', '2018-02-05 星期一', '2018-03-09 星期五', '2018-01-12 星期五', '2018-03-05 星期一', '2018-03-19 星期一', '2018-01-15 星期一', '2018-02-09 星期五', '2018-01-19 星期五', '2018-02-12 星期一', '2018-03-23 星期五'] # 日期+星期,同平台格式
})
amt = df_rank.groupby('商品名称')['实收金额'].sum().rename('实收金额总和') # 金额口径:与平台任务1判定代码完全一致
qty = df_rank.groupby('商品名称')['销售数量'].sum().rename('销售数量总和') # 数量口径:仅把聚合列换成销售数量,其余代码不动
compare = pd.concat([qty, amt], axis=1) # 两种口径并排放进同一张表
compare['金额口径名次'] = compare['实收金额总和'].rank(ascending=False, method='min').astype(int) # 按金额总和排名
compare['数量口径名次'] = compare['销售数量总和'].rank(ascending=False, method='min').astype(int) # 按销售数量排名
print(compare.sort_values('金额口径名次')) # 输出对比表,观察两个名次列的错位
print('\n金额口径的销售冠军:', compare['实收金额总和'].idxmax(), '| 数量口径的销售冠军:', compare['销售数量总和'].idxmax()) # 两种口径各自的冠军预期输出(本机实际运行结果):
销售数量总和 实收金额总和 金额口径名次 数量口径名次
商品名称
开博通 18 1102.60 1 2
氨苄西林胶囊 9 301.32 2 3
VC银翘片 19 194.94 3 1
TG厄贝沙坦片 3 59.40 4 4
金额口径的销售冠军: 开博通 | 数量口径的销售冠军: VC银翘片
两种口径给出了两个不同的”销售冠军”:按实收金额是开博通(1102.60元),按销售数量是VC银翘片(19盒),四种药品中有三种的名次发生了移动。名次错位的根源是单价:高价药以金额取胜,低价常用药以盒数取胜。这正是”口径先于代码”的具体含义——回答”哪种药占用患者最多药费、贡献最多收入”用金额口径;回答”哪种药使用频次最高、需要按盒备货”用数量口径。到平台上做题时,题面提示与判定代码口径不一致,以平台判定为准;但在自己的分析报告里,应当先声明口径再报排名。
24.5 任务2时间变化趋势
# 注:本块是平台任务对应部分的分步讲解版(重述平台代码,非新增分析)。该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)
# =============================================================================
# 题目:销售额的时间序列趋势分析
# =============================================================================
# 本代码分析整体销售额随时间的变化趋势,包括月度趋势和星期趋势。
# 时间序列分析可以识别季节性规律、周期性波动和异常点,为销售预测
# 和资源规划提供依据。
# ==================== 提取月份特征 ====================
# 从购药日期列中提取月份
df['购药日期_月'] = df['购药日期'].str.split(pat='-', expand=True)[1]
# str.split('-')按短横线分割日期字符串
# expand=True将分割结果展开为DataFrame
# [1]取第二列(月份部分),例如:"2018-01-01" → "01"
# ==================== 绘制月度销售趋势 ====================
# 创建画布,尺寸为15x10英寸
plt.figure(figsize=(15, 10))
# 使用Seaborn绘制月度销售趋势线图
sns.lineplot(x='购药日期_月', y='实收金额', data=df, estimator='sum', errorbar=None)
# x='购药日期_月'指定x轴数据(月份)
# y='实收金额'指定y轴数据(销售金额)
# data=df指定数据源
# estimator='sum'表示对每个月的销售额求和
# errorbar=None表示不显示误差条(置信区间)
# 设置图表标题
plt.title('各月销售额变化趋势', fontsize=16) # 标题说明分析内容
# 设置x轴标签
plt.xlabel('月份', fontsize=12) # x轴表示月份(1-12)
# 设置y轴标签
plt.ylabel('销售金额(元)', fontsize=12) # y轴表示销售金额,注明单位
# 添加网格线,便于读取数值
plt.grid(True, alpha=0.3) # alpha=0.3设置网格线透明度
# 自动调整布局
plt.tight_layout() # 防止标签被截断
# 显示图表
plt.show() # 展示完整的月度趋势图
# ==================== 绘制星期销售趋势 ====================
# 创建新的画布
plt.figure(figsize=(15, 10))
# 使用Seaborn绘制星期销售趋势线图
sns.lineplot(x='购药星期', y='实收金额', data=df, estimator='sum', errorbar=None)
# x='购药星期'指定x轴数据(星期几)
# y='实收金额'指定y轴数据(销售金额)
# estimator='sum'表示对每个星期的销售额求和
# errorbar=None不显示误差条
# 设置图表标题
plt.title('各星期销售额变化趋势', fontsize=16)
# 设置x轴标签
plt.xlabel('星期', fontsize=12) # x轴表示星期(星期一至星期日)
# 设置y轴标签
plt.ylabel('销售金额(元)', fontsize=12)
# 添加网格线
plt.grid(True, alpha=0.3)
# 自动调整布局
plt.tight_layout()
# 显示图表
plt.show() # 展示完整的星期趋势图
# ==================== 输出解读 ====================
# 时间趋势分析揭示销售的时间规律:
#
# 1. 月度趋势:
# - 识别季节性规律:某些月份销售显著高于其他月份
# - 可能原因:流感季(冬春)、过敏季(春季)、寒暑假(学生购药减少)
# - 用途:根据预测调整库存,避免缺货或积压
#
# 2. 星期趋势:
# - 工作日vs周末:工作日销售可能更高(上班族就诊)
# - 周初vs周末:周一可能最高(周末积累的病人)
# - 用途:合理安排医生排班和药房人手
#
# 3. 异常点:
# - 突然的峰值:可能因疫情爆发、流感流行
# - 突然的低谷:可能因节假日、医院停诊
# - 需要结合外部事件分析
#
# 4. 业务决策:
# - 库存管理:高峰月提前备货
# - 人员安排:高峰日增加人手
# - 营销策略:淡季推出促销活动
# - 预测模型:基于历史数据预测未来销售24.6 任务3各月份最佳药品
# 注:本块是平台任务对应部分的分步讲解版(重述平台代码,非新增分析)。该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)
# =============================================================================
# 题目:识别每个月的销售冠军药品
# =============================================================================
# 本代码分析每个月销售最好的药品,识别季节性畅销品。不同季节可能
# 伴随不同的疾病,导致相关药品销售上升。这种分析可以指导季节性
# 备货和营销策略。
# ==================== 获取所有月份 ====================
# 提取购药日期_月列的所有唯一值(去重后的月份列表)
months = df['购药日期_月'].unique() # unique()返回唯一值数组
# 例如:['01', '02', '03', ..., '12']
# ==================== 遍历每个月,找出最佳药品 ====================
for month in sorted(months): # sorted()对月份排序,确保按1-12的顺序输出
# 筛选该月份的所有销售记录
df_month = df[df['购药日期_月'] == month] # 布尔索引,筛选特定月份的行
# 按月份和商品名称分组,计算销售金额,找出第一名
df_best = df_month.groupby(['购药日期_月', '商品名称'])['实收金额'].sum().nlargest(1).reset_index()
# groupby(['购药日期_月', '商品名称'])按月份和药品分组
# ['实收金额']选择要聚合的列
# .sum()计算每组的销售总额
# .nlargest(1)提取销售金额最大的1行(即该月的销售冠军)
# .reset_index()重置索引
# 打印该月的最佳药品
print(f'{month}月最佳药品:')
print(df_best) # 显示月份、药品名称、销售金额
print() # 打印空行,分隔不同月份的输出
# ==================== 输出解读 ====================
# 各月最佳药品分析揭示季节性用药规律:
#
# 1. 季节性疾病:
# - 冬季(12-2月):感冒药、退烧药销售冠军
# - 春季(3-5月):抗过敏药销售冠军
# - 夏季(6-8月):消化药、防暑药销售冠军
# - 秋季(9-11月):止咳药、抗生素销售冠军
#
# 2. 备货策略:
# - 提前1个月备货季节性药品
# - 避免高峰期缺货影响患者
# - 减少非季节性药品的库存积压
#
# 3. 营销策略:
# - 在季节到来前推出相关促销
# - 制作季节性用药指南,指导患者
# - 与医生沟通,提前处方调整
#
# 4. 采购决策:
# - 与供应商签订季节性供货协议
# - 确保高峰期的药品供应
# - 争取批量采购的折扣24.7 任务4社保减免分析
# 注:本块是平台任务对应部分的分步讲解版(重述平台代码,非新增分析)。该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)
# =============================================================================
# 题目:分析社保减免对药品购买意愿的影响
# =============================================================================
# 本代码通过对比应收金额和实收金额,判断药品是否使用了社保减免,
# 并统计使用社保减免的药品占比。这有助于了解患者对医保政策的响应,
# 以及医保政策对药品销售的影响。
# ==================== 计算差额 ====================
# 计算应收金额和实收金额的差额
df['差额'] = df['应收金额'] - df['实收金额']
# 应收金额:药品原价
# 实收金额:患者实际支付的金额
# 差额:社保报销的金额
# 差额>0表示使用了社保减免,差额=0表示未使用
# ==================== 判断是否使用社保 ====================
# 根据差额判断是否使用社保减免
df['是否社保减免药品'] = df['差额'].apply(lambda x: '是' if x != 0 else '否')
# apply()函数对Series的每个元素应用指定函数
# lambda x: '是' if x != 0 else '否'是一个匿名函数
# 如果差额不为0,返回'是';否则返回'否'
# 结果是一个新的分类变量
# ==================== 统计社保减免比例 ====================
# 计算'是'和'否'的数量和占比
insurance_ratio = df['是否社保减免药品'].value_counts(normalize=True)
# value_counts()计算每个类别的数量
# normalize=True返回占比而非绝对数量
# 结果格式: {'是': 0.75, '否': 0.25}
# 打印社保减免药品的占比
print('社保减免药品占比:')
print(insurance_ratio) # 显示'是'和'否'的占比
# 提取'是'的占比,并转换为百分比
print(f'\n使用社保减免的药品占比: {insurance_ratio.get("是", 0)*100:.1f}%')
# insurance_ratio.get("是", 0)获取'是'的占比,如果不存在则返回0
# * 100转换为百分比
# :.1f保留1位小数
# ==================== 输出解读 ====================
# 社保减免分析揭示医保政策的影响:
#
# 1. 占比解读:
# - 如果社保减免占比很高(如75%以上),说明:
# - 患者倾向于使用医保购买药品
# - 医保政策覆盖面广,患者受益大
# - 药品大部分在医保目录内
#
# 2. 患者行为:
# - 价格敏感度:有医保减免时,患者更愿意购买
# - 药品选择:优先选择医保目录内的药品
# - 购买意愿:医保降低了经济负担,增加了用药需求
#
# 3. 医院策略:
# - 药品采购:优先采购医保目录内的药品
# - 处方管理:医生优先开医保药品,提高患者满意度
# - 库存管理:医保药品库存要充足
#
# 4. 政策影响:
# - 医保目录调整会影响药品销售
# - 报销比例变化会影响患者选择
# - 需要密切关注医保政策变化
#
# 5. 商业机会:
# - 对于非医保药品,需要突出其独特价值
# - 可以开发医保目录外的替代药品
# - 提供自费患者的增值服务24.8 分析结论
- 销售冠军: 开博通
- 销售垫底: TG厄贝沙坦片
- 季节性: 药品销售与时间相关
- 医保影响: 社保减免显著影响购买意愿
24.9 本章小结
本章新增的技能要点:
- 字符串时间列的两步拆解:先按空格切出”日期+星期”,再按”-“取出月份,得到可供分组的时间维度。
- 排名类问题优先用
nlargest/nsmallest:比”排序后取头”意图更直接,也避免改动原表。 sns.lineplot(estimator='sum'):对分类x轴直接做分组求和绘图,省去先聚合再画的步骤。- 口径先于代码:题面提示写”销售数量总和”、判定代码却按”实收金额总和”聚合,动手前必须对齐口径(以平台判定为准)。
- 差额构造标志列:“应收-实收”是否为0是本章判断社保减免的依据,这类”由两列推导第三列”的思路在业务数据中反复出现。
易错点:
- 不核对口径就按题面提示的”销售数量”聚合:平台判定按”实收金额总和”,金额口径偏高价药、数量口径偏低价常用药,两个口径会给出不同的”销售冠军”
sns.lineplot漏写estimator='sum':默认统计量是均值,画出的折线是各月平均单笔金额而非月度总额- 提取月份时越级取错段:须先按空格切出日期、再按”-“取第2段;直接对”购药时间”整串取第3段会把”05 星期五”连日期带星期混进月份列
- 任务3循环体内的缩进错误是平台题面原样:不要”顺手修正”后再提交,平台只认原始题面
- 并列名次跨越截断点时,
nlargest(2)与sort_values().head(2)都不扩展并列,后者默认非稳定排序、并列顺序无承诺;要”并列都取”须改用rank(method='min') - 把”社保减免药品占比高”直接读成”人们更愿意购买可社保减免的药品”:占比只说明销售构成,推断购买意愿还须交代按订单数还是金额、完全自费还是部分自费的口径
24.10 动手与思考
以下练习每题附参考答案(默认折叠);概念题与变式题可对照自查,商业判断题不设唯一答案,判断依据与口径比结论更重要。
概念辨析:“销售情况最好”用实收金额总和与用销售数量总和衡量,结论可能有何不同?两种口径分别适合回答什么业务问题?
参考答案(点开前请先独立完成)
解题思路:两种口径可能给出不同的“销售冠军”。金额口径被高价药主导——单价高的药品即使销量不大,金额总和也容易居前;数量口径被低价常用药主导——单盒便宜的常用药开得最频繁。本章“任务1”一节的变式实验(列表 24.3)在合成数据上实测过这种错位:按实收金额总和,开博通第一;按销售数量总和,VC银翘片第一,四种药品里有三种名次发生移动。口径选择跟着业务问题走:要回答“哪种药占用患者最多药费、贡献最多收入”用金额口径;要回答“哪种药使用频次最高、需要按盒备货”用数量口径。题面提示与平台判定代码口径不一致时以平台判定为准,但在自己的分析报告里应当先声明口径再报排名。
回扣本章:对应“本章新增的技能要点”第4条(口径先于代码)。
概念辨析:
df_sum.nlargest(2, '实收金额')与df_sum.sort_values('实收金额', ascending=False).head(2)在什么情况下结果一致、什么情况下不一致(提示:并列名次)?参考答案(点开前请先独立完成)
解题思路:当“实收金额”取值在截断点附近没有并列时(比如前几名金额互不相同),两种写法返回的行集合与顺序完全一致。当并列跨越截断点时,两者都只返回2行、不做并列扩展,差别在并列行的取舍规则:
nlargest有显式的keep参数(默认keep='first',并列时保留先出现的行),而sort_values默认使用非稳定排序(kind默认为 ‘quicksort’),对并列值的先后顺序不做承诺——同一份小数据上可能恰好与nlargest一致,换一份数据就可能取到另一条并列行或行序不同(实测给sort_values传kind='stable'之后,两者行序完全一致)。如果业务要求“并列的都取”,两种写法都不满足,应改用rank(ascending=False, method='min')先标记同名次、再按名次筛选。综合来看,nlargest把并列取舍写成了明确规则、意图也更直接,这是本章把它列为排名首选写法的原因。回扣本章:对应“本章新增的技能要点”第2条(排名类问题优先用
nlargest/nsmallest,行为规则明确且不改动原表)。变式任务:把任务1的评定指标换成销售数量总和,重做TOP10柱状图——需要修改哪些代码行?
参考答案(点开前请先独立完成)
改造思路:任务1的代码里所有出现“实收金额”的地方共三处需要同步替换——
groupby后的聚合列、nlargest/nsmallest的排序依据列、plt.bar的柱高数据列;变量名建议同步改为df_qty,避免与原任务的df_sum混淆。数据读取、缺失值处理等前段一行不动。# 变式代码:任务1的评定指标由实收金额换成销售数量(仅列改动行,其余与平台任务一致) df_qty = df.groupby('商品名称')['销售数量'].sum().reset_index() # ← 改动点1:聚合列由“实收金额”改为“销售数量” print(df_qty.nlargest(2, '销售数量')) # ← 改动点2:销售最好两名的取数列同步替换 print(df_qty.nsmallest(2, '销售数量')) # ← 改动点2:销售最差两名同上 df_qty_n10 = df_qty.nlargest(10, '销售数量') # ← 改动点2:TOP10取数列同步替换 plt.figure(figsize=(15, 10)) # 与平台任务一致 plt.grid() # 与平台任务一致 plt.bar(df_qty_n10['商品名称'], df_qty_n10['销售数量']) # ← 改动点3:柱高改为销售数量列 plt.savefig('1_qty.png') # 建议另存新文件名,避免覆盖平台任务的1.png plt.close() # 与平台任务一致结构性判读:图形仍是一张TOP10柱状图,但柱高含义由“实收金额总和”变为“销售数量总和”,柱的排序与高低差随之改变。判读要点:与原任务的1.png对照——头部药品是否换了名、量级断层是否移动;若两种口径的冠军不同,说明存在低价高频药与高价低频药的分化,备货口径(按盒)与营收口径(按元)应分别参考各自的排名。
本地演练版(模拟数据):在与正文变式实验同一份15行合成数据上实跑(列结构同平台数据):
# 本地演练版:销售数量口径的药品排名(数据与正文@lst-ch24-ranking-metric-variant一致) import pandas as pd # 导入Pandas数据分析库 df_rank = pd.DataFrame({ # 15行合成数据:VC银翘片为低价常用药、开博通为高价药,以放大两种口径的差异 '商品名称': ['开博通', '开博通', '开博通', '开博通', '氨苄西林胶囊', '氨苄西林胶囊', '氨苄西林胶囊', 'TG厄贝沙坦片', 'TG厄贝沙坦片', 'TG厄贝沙坦片', 'VC银翘片', 'VC银翘片', 'VC银翘片', 'VC银翘片', 'VC银翘片'], '销售数量': [5, 3, 6, 4, 2, 4, 3, 1, 1, 1, 2, 2, 5, 5, 5], # 每笔销售的盒数 '实收金额': [281.0, 189.6, 379.2, 252.8, 66.96, 133.92, 100.44, 19.8, 19.8, 19.8, 20.52, 20.52, 51.3, 51.3, 51.3] # 实收金额 }) df_qty = df_rank.groupby('商品名称')['销售数量'].sum().reset_index() # 变式改动点1:聚合列改为销售数量 print('数量口径的药品排名(降序,柱状图将按此顺序绘柱):') print(df_qty.sort_values('销售数量', ascending=False).reset_index(drop=True)) # 变式改动点2:排序与柱高均改用数量列 print('数量口径TOP2:', df_qty.nlargest(2, '销售数量')['商品名称'].tolist()) # 与平台任务1同型的TOP取数模拟数据演练结果(本机 peter 环境实际运行结果,仅演示流程与判读方法,真实数值以平台运行结果为准):
数量口径的药品排名(降序,柱状图将按此顺序绘柱): 商品名称 销售数量 0 VC银翘片 19 1 开博通 18 2 氨苄西林胶囊 9 3 TG厄贝沙坦片 3 数量口径TOP2: ['VC银翘片', '开博通']演练判读:数量口径的冠军换成VC银翘片(19盒),与正文中金额口径冠军开博通不同——同一份数据、两种口径、两个“销售冠军”,印证了第1题的结论。
预期输出:数据文件由教学平台内置,本机无法复现,具体数值以平台运行结果为准。
注意:以上为本题变式的独立代码;列表 24.1 对应平台任务的原始代码块仍须原样输入教学平台,不要用本变式替换。
回扣本章:对应“本章新增的技能要点”第2条(
nlargest换一个排序依据列即可切换口径)与第4条(口径先于代码)。变式任务:把任务2的星期维度换成”上旬/中旬/下旬”三段,特征构造与绘图的改造思路是什么?
参考答案(点开前请先独立完成)
改造思路:分两步。特征构造上,沿用任务2“字符串两步拆解”的思路再多走一步:先按空格切出日期段,再按“-”取出“日”并转成整数,然后用
pd.cut把1—10日、11—20日、21—31日分别标为上旬、中旬、下旬(bins=[0, 10, 20, 31],右端闭合,恰好覆盖大月的31日)。绘图上,sns.lineplot的x由“购药星期”换成新的“旬”列,estimator='sum'不变。# 变式代码:星期维度换成“上旬/中旬/下旬”(特征构造新增两行,绘图改一处) df['购药日期'] = df['购药时间'].str.split(pat=' ', expand=True)[0] # 与平台任务一致:先按空格切出日期段 df['购药日'] = df['购药日期'].str.split(pat='-', expand=True)[2].astype(int) # ← 新增行:再按“-”取出日并转整数 df['旬'] = pd.cut(df['购药日'], bins=[0, 10, 20, 31], labels=['上旬', '中旬', '下旬']) # ← 新增行:1—10上旬、11—20中旬、21—31下旬 plt.figure(figsize=(15, 10)) # 与平台任务一致 plt.grid() # 与平台任务一致 sns.lineplot(x='旬', y='实收金额', data=df, estimator='sum') # ← 改动点:x轴由“购药星期”换成“旬” plt.savefig('2_tenth.png') # 建议另存新文件名,避免覆盖平台任务的2.png plt.close() # 与平台任务一致结构性判读:图形由7个星期取值的折线变为仅3个取值(上旬、中旬、下旬)的折线,纵轴仍是各段实收金额总和。判读要点:三段高度的相对关系——月内前中后的销售节奏是否均衡,中旬偏高可能与复诊开药周期有关;由于只有3个点,结论应与月度折线(2.png)互证,而不是单独下判断。
本地演练版(模拟数据):在与正文本地演练同结构的12行合成数据上实跑:
# 本地演练版:构造“旬”特征并按旬汇总实收金额 import pandas as pd # 导入Pandas数据分析库 df_mini = pd.DataFrame({ # 12行迷你表,购药时间为“日期 空格 星期”格式,同平台数据 '商品名称': ['开博通', '氨苄西林胶囊', 'TG厄贝沙坦片', '开博通', '氨苄西林胶囊', 'VC银翘片', '开博通', 'TG厄贝沙坦片', '氨苄西林胶囊', 'VC银翘片', '开博通', 'TG厄贝沙坦片'], '实收金额': [281.0, 66.96, 19.8, 189.6, 133.92, 45.6, 379.2, 19.8, 100.44, 45.6, 252.8, 19.8], # 实收金额 '购药时间': ['2018-01-05 星期五', '2018-01-28 星期日', '2018-02-02 星期五', '2018-02-14 星期三', '2018-02-25 星期日', '2018-03-02 星期五', '2018-03-11 星期日', '2018-03-22 星期四', '2018-03-25 星期日', '2018-04-03 星期二', '2018-04-18 星期三', '2018-04-29 星期日'] # 日期+星期 }) df_mini['购药日期'] = df_mini['购药时间'].str.split(pat=' ', expand=True)[0] # 与平台任务一致:先切出日期段 df_mini['购药日'] = df_mini['购药日期'].str.split(pat='-', expand=True)[2].astype(int) # 变式新增:取出“日”并转整数 df_mini['旬'] = pd.cut(df_mini['购药日'], bins=[0, 10, 20, 31], labels=['上旬', '中旬', '下旬']) # 变式新增:划分上中下旬 tenth_table = df_mini.groupby('旬', observed=True)['实收金额'].sum() # 变式新增:按旬汇总实收金额 print('上旬/中旬/下旬的实收金额汇总:') print(tenth_table) # 输出汇总表 print('各旬订单数:') print(df_mini['旬'].value_counts()) # 各旬订单笔数模拟数据演练结果(本机 peter 环境实际运行结果,仅演示流程与判读方法,真实数值以平台运行结果为准):
上旬/中旬/下旬的实收金额汇总: 旬 上旬 392.00 中旬 821.60 下旬 340.92 Name: 实收金额, dtype: float64 各旬订单数: 旬 下旬 5 上旬 4 中旬 3 Name: count, dtype: int64演练判读:迷你表中旬金额最高而下旬订单数最多——金额口径与订单数口径的“最高段”可以不同,读图时先分清纵轴是哪一个。
预期输出:数据文件由教学平台内置,本机无法复现,具体数值以平台运行结果为准。
注意:以上为本题变式的独立代码;列表 24.1 对应平台任务的原始代码块仍须原样输入教学平台,不要用本变式替换。
回扣本章:对应“本章新增的技能要点”第1条(字符串时间列的逐步拆解:空格切日期、短横线取月份,本变式再多取一位“日”)。
商业判断:若平台结果显示某种非医保药品连续数月进入销售TOP3(具体名次以平台运行结果为准),医院在采购与处方管理上应如何决策?请兼顾医保占比、患者自费负担与药事管理要求。
参考答案(点开前请先独立完成)
参考作答框架:本题不设唯一结论,优秀作答应覆盖以下维度。
- 口径先行:TOP3按什么口径排名(平台判定按实收金额总和,见@lst-ch24-platform-task,题面提示曾写数量口径);“医保占比”按订单数还是金额算;“非医保”是完全自费还是部分自费——三种口径不清,后续判断都站不住。
- 分析依据:连续数月进入TOP3说明存在持续的真实需求或处方习惯,应先区分是临床刚需、可替代药,还是医生处方偏好;任务4的差额口径(应收减实收)可帮助核实该药确实几乎不走社保减免。
- 风险考量:采购上,单品种连续高位意味着供应集中风险,备货需与临床需求联动;处方管理上,非医保药高位销售可能加重患者自费负担、影响依从性,也可能触发药事管理与医保监管的合规关注(如辅助用药、抗菌药管理要求)。
- 模型边界:月度TOP3是描述性排名,不等于因果结论;连续数月也可能由季节性疾病、短期短缺替代等一次性因素驱动,需要更长窗口确认持续性。
- 还需补充的数据:该药的适应症与诊断分布、同类医保可替代药品的价格与销量、患者自费放弃率与复购率、医保目录调整计划、供应商供货周期。
- 常见错误作答形态:直接建议“扩大采购”而不做临床与合规审查;或仅因“非医保”就建议限制使用而不顾临床需求;用一个月的排名当作长期趋势。
回扣本章:对应“本章新增的技能要点”第4条(口径先于代码,排名结论必须连同口径一起报告)。